/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package netwrokproject.transporte;

import java.util.ArrayDeque;

import netwrokproject.redes.Rede;

/**
 *
 * @author Clodoaldo
 */
public class Porta extends Thread {

    // PROPRIEDADES PARA CONTROLE DE RETRANSMISSAO
    static final int MAXIMO_TENTATIVAS = 2;
    static final long TIMEOUT_LIMITE = 120000000;
    int tentativas = 0;
    RTOTimer timer = null;

    // FLAGS DO PACOTE
    static final byte NONE = 0;
    static final byte SYN = 1;
    static final byte FIN = 2;
    static final byte RST = 4;
    static final byte ACK = 8;
    static final byte DATA = 16;

    // ENDEREÇOS DE CONEXÃO
    private byte ipLocal;
    private byte portaLocal;
    private byte ipRemoto;
    private byte portaRemota;

    // ESTADOS DA CONEXÃO
    boolean conectado = false;
    boolean escutando = false;
    boolean pedindoconexao = false;

    // FILA PARA ACESSO DA APLICAÇÃO
    ArrayDeque<ArrayDeque<Byte>> MensagensRecebidas = new ArrayDeque<ArrayDeque<Byte>>();

    // CONTROLE DE TRANSMISSAO E FLAGS
    short Ack = 0;
    short Sequencia = 0;
    byte flagEsperado = 0;

    private Transporte pai;

    public Porta(byte porta, Transporte t) {
        this.portaLocal = porta;
        this.ipLocal = Byte.parseByte((((Rede)t.getLayerDOWN()).getOrigem()));
        this.pai = t;
    }

    @Override
    public void run() {
        try {
            while (true) {
                // TODO adicionar semaforo para nao cagar desempenho do programa
                receiveMessage();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (o.getClass() == this.getClass())
            if (((Porta)o).portaLocal == this.portaLocal)
                return true;
        return false;
    }

    @Override
    public int hashCode() {
        int hash = 3;
        hash = 13 * hash + this.ipLocal;
        hash = 13 * hash + this.portaLocal;
        hash = 13 * hash + this.ipRemoto;
        hash = 13 * hash + this.portaRemota;
        return hash;
    }

    public void listen() throws IllegalStateException {

        if (conectado) throw new IllegalStateException("listen(): Conexao ja" +
                " estabelecida.");
        System.out.println("listen(): " + ipLocal + ":" + portaLocal +
                " na escuta.");
        escutando = true;
    }

    private void checaack(MensagemTransporte pacotetemp) {
    	System.out.println("checaack(): ACK recebido - " + pacotetemp.getAck());
        try {
            pai.LimparCandidatas(pacotetemp.ack);
            pai.retransmitir();
        } catch (Exception ex) {
        	if (ex.getMessage().equals("Lista de canditadas a retransmissao vazia.")) {
        		return;
        	}
            ex.printStackTrace();
        }
    }

    private void reset(byte ipDestino, byte portaDestino) {

        byte flags = RST;
        MensagemTransporte msg = new MensagemTransporte(ipLocal, portaLocal,
                ipDestino, portaDestino, (short) 0, (short) 0, flags,
                new ArrayDeque<Byte>());

        sendmsg(msg, NONE);
        conectado = false;
        escutando = false;
        Sequencia = 0;

    }

    public void connect(byte ipDestino, byte portaDestino) throws IllegalStateException {

        if (conectado) throw new IllegalStateException("connect(): " +
                "Conexao ja estabelecida.");

        this.ipRemoto = ipDestino;
        this.portaRemota = portaDestino;

        System.out.println("connect() - "+ ipLocal + ":" + portaLocal +
                " conectando a "+ ipRemoto + ":" + portaRemota +".");

        Sequencia = 0;
        Ack = 0;

        byte flags = SYN;

        MensagemTransporte msg = new MensagemTransporte(ipLocal, portaLocal,
                ipRemoto, portaRemota, Sequencia++, ++Ack, flags,
                new ArrayDeque<Byte>());

        sendmsg(msg, ((byte) (SYN | ACK)));

    }

    private void connectionaccept() {

        System.out.println("aceitarconexao(): pedido de conexao de " + ipRemoto +
                ":" + portaRemota + " aceito. Enviando confirmacao.");
        byte flags = SYN | ACK;

        MensagemTransporte msg = new MensagemTransporte(ipLocal, portaLocal,
                ipRemoto, portaRemota, Sequencia++, ++Ack, flags,
                new ArrayDeque<Byte>());

        sendmsg(msg, ACK);
        escutando = false;
        conectado = true;

    }

    public void disconnect() throws IllegalStateException {

        if (!conectado) throw new IllegalStateException("desconectar() - Sem conexao.");

        System.out.println("desconectar(): iniciando desconexao.");

        byte flags = FIN;

        MensagemTransporte msg = new MensagemTransporte(ipLocal, portaLocal,
                ipRemoto, portaRemota, Sequencia++, ++Ack, flags,
                new ArrayDeque<Byte>());

        sendmsg(msg, (byte)(FIN | ACK));

        Sequencia = 0;
    }

    private void disconnectionaccept() throws IllegalStateException {

        if (!conectado) throw new IllegalStateException("aceitardesconexao() - Sem conexao.");

        System.out.println("aceitardesconexao(): Pedido de desconexao recebido, desconectando.");

        byte flags = FIN | ACK;

        MensagemTransporte msg = new MensagemTransporte(ipLocal, portaLocal,
                ipRemoto, portaRemota, Sequencia++, ++Ack, flags,
                new ArrayDeque<Byte>());

        sendmsg(msg, ACK);

        Sequencia = 0;
    }

    private void sendmsg(MensagemTransporte msg, byte flagsEsperados) {

        pai.msgsToBeSent.addLast(msg);
        this.flagEsperado = flagsEsperados;
        this.Sequencia++;
        System.out.println("sendmsg(): Mensagem Encaminhada.");
        pai.getLayerDOWN().send(msg.toArrayDeque());
    }

    public ArrayDeque<Byte> receive(){
        return MensagensRecebidas.removeFirst();
    }

    public void send(ArrayDeque<Byte> dados) {
        MensagemTransporte msg = new MensagemTransporte(ipLocal, portaLocal, ipRemoto, portaRemota, Sequencia++, ++Ack, DATA, dados);
        sendmsg(msg, ACK);
    }

    protected synchronized void  receiveMessage() {

        //System.out.println("receiveMessage(): mensagem chegando.");

        while(!MensagensRecebidas.isEmpty()) {
            MensagemTransporte pacotetemp = new MensagemTransporte
                    (MensagensRecebidas.removeFirst());

            // Checa se o pacote é para este host, que já está conectado
            if (pacotetemp.ipOrigem == this.ipRemoto &&
                pacotetemp.portaOrigem == this.portaRemota &&
                conectado) {

                // Checa se há um pedido de desconexão no pacote
                if ((pacotetemp.flags & FIN) == FIN && (!((pacotetemp.flags & (FIN | ACK)) == (FIN | ACK)))) {
                    // Aceita desconexao e envia confirmacao
                    disconnectionaccept();
                    Ack++;
                }
                
                if (((pacotetemp.flags) & (FIN | ACK)) == (FIN | ACK)){
                	// Envia ack de desconexao
                	conectado = false;
                	checaack(pacotetemp);
                }

                if ((pacotetemp.flags & ACK) == ACK) {
                    checaack(pacotetemp);
                }

                if ((pacotetemp.flags & DATA) == DATA){
                    // Pacote de Dados
                    System.out.println("ReceiveMessage: Recebido dados - " + pacotetemp.getDados());
                    if ((pacotetemp.flags & ACK) == ACK) {
                        checaack(pacotetemp);
                    }
                }

            } else {

                // Checa se o pacote é para este host, que está esperando conexão
                if (pacotetemp.ipDestino == this.ipLocal &&
                    pacotetemp.portaDestino == this.portaLocal &&
                    escutando){
                        System.out.println("receiveMessage(): Entrou na escuta.");
                        // Checando se o pacote é um pedido de conexão
                        if (pacotetemp.getFlags() == SYN){
                            this.ipRemoto = pacotetemp.ipOrigem;
                            this.portaRemota = pacotetemp.portaOrigem;
                            connectionaccept();
                        } else {
                            // Pedido estranho, responder com reset (RST)
                            reset(pacotetemp.ipOrigem, pacotetemp.portaOrigem);
                        }
                } else {

                    if (pacotetemp.ipDestino == this.ipLocal &&
                    pacotetemp.portaDestino == this.portaLocal) {
                        if (pacotetemp.getFlags() == (SYN | ACK) &&
                        flagEsperado == (SYN | ACK)) {
                            connectionaccept();
                        }
                    } else {
                        // Falha de segurança: qqer host pode dar reset, desde q
                        // conheça a porta aberta! :P
                        if (pacotetemp.flags == RST &&
                                pacotetemp.ipDestino == this.ipLocal &&
                                pacotetemp.portaDestino == this.portaLocal) {
                            escutando = false;
                            conectado = false;
                        }
                        else {
                            reset(pacotetemp.ipOrigem, pacotetemp.portaOrigem);
                        }
                    }
                }
            }
        }

    }

    // GETS E SETS

    public byte getFlagEsperado() {
        return flagEsperado;
    }

    public void setFlagEsperado(byte flagEsperado) {
        this.flagEsperado = flagEsperado;
    }

    public boolean isConectado() {
        return conectado;
    }

    public byte getIpLocal() {
        return ipLocal;
    }

    public void setIpLocal(byte ipLocal) {
        this.ipLocal = ipLocal;
    }

    public byte getIpRemoto() {
        return ipRemoto;
    }

    public void setIpRemoto(byte ipRemoto) {
        this.ipRemoto = ipRemoto;
    }

    public byte getPortaLocal() {
        return portaLocal;
    }

    public void setPortaLocal(byte portaLocal) {
        this.portaLocal = portaLocal;
    }

    public byte getPortaRemota() {
        return portaRemota;
    }

    public void setPortaRemota(byte portaRemota) {
        this.portaRemota = portaRemota;
    }

    public short getAck() {
        return Ack;
    }

    public void setAck(short Ack) {
        this.Ack = Ack;
    }

    public short getSequencia() {
        return Sequencia;
    }

    public void setSequencia(short Sequencia) {
        this.Sequencia = Sequencia;
    }
}


